package info.niwota.ziplock.zip;

import info.niwota.ziplock.Debug;
import info.niwota.ziplock.FileItem;
import info.niwota.ziplock.StatusMonitor;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import android.util.Log;

/**
 * @author qiangli
 * 
 */

public final class CryptoUtils {
	private static final int BUFSIZE = 1024;

	private final static int LEN = 16;
	private final static int ITERATION_COUNT = 1001;

	private static final String PBE_ALG = "PBEWithMD5AndDES";

	// PBEWithMD5AndDES
	// PBEWithMD5AndTripleDES
	// PBEWithSHA1AndDESede

	private static final int KEYSIZE = 128;

	private static final String TAG = "CryptoUtils";

	// 256 192 128

	private static void copy(InputStream in, OutputStream out)
			throws IOException {
		byte[] buf = new byte[BUFSIZE];
		int read = 0;
		while ((read = in.read(buf)) >= 0) {
			out.write(buf, 0, read);
		}
	}

	public static Cipher createCipher(int mode, Key key) throws Exception {
		Cipher cipher = Cipher.getInstance("AES");
		cipher.init(mode, key);
		return cipher;
	}

	public static void decrypt(File in, File out, Key key) throws Exception {
		Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key);
		//
		FileInputStream fis = new FileInputStream(in);
		BufferedInputStream bis = new BufferedInputStream(fis, BUFSIZE);
		CipherInputStream cis = new CipherInputStream(bis, cipher);
		//
		FileOutputStream fos = new FileOutputStream(out);
		BufferedOutputStream bos = new BufferedOutputStream(fos, BUFSIZE);
		copy(cis, bos);
		//
		bos.flush();
		bos.close();
		cis.close();
	}

	public static String decrypt(String encrypted, Key key) throws Exception {
		Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key);
		//
		byte[] b = Base64.decode(encrypted);
		byte[] c = cipher.doFinal(b);
		//
		return new String(c);
	}

	public static String digest(String password) throws Exception {
		final String algorithm = "SHA-1";
		MessageDigest md = MessageDigest.getInstance(algorithm);
		md.reset();
		md.update(password.getBytes());
		byte[] b = md.digest();
		//
		return Base64.encodeBytes(b);
	}

	public static void encrypt(File in, File out, Key key) throws Exception {
		Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, key);
		//
		FileOutputStream fos = new FileOutputStream(out);
		BufferedOutputStream bos = new BufferedOutputStream(fos, BUFSIZE);
		CipherOutputStream cos = new CipherOutputStream(bos, cipher);
		//
		FileInputStream fis = new FileInputStream(in);
		BufferedInputStream bis = new BufferedInputStream(fis, BUFSIZE);
		copy(fis, cos);
		//
		cos.flush();
		cos.close();
		bis.close();
	}

	public static String encrypt(String cleartext, Key key) throws Exception {
		Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, key);
		//
		byte[] b = cleartext.getBytes();
		byte[] c = cipher.doFinal(b);
		String enc = Base64.encodeBytes(c);
		//
		return enc;
	}

	public static SecretKey genAESKey() throws Exception {
		KeyGenerator kg = KeyGenerator.getInstance("AES");
		SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
		kg.init(KEYSIZE, sr);
		SecretKey skey = kg.generateKey();
		return skey;
	}

	public static String[] genKey() throws Exception {
		String pwd = tempPassword();
		return genKey(pwd);
	}

	public static String[] genKey(String password) throws Exception {
		String salt = salt();
		SecretKey skey = genAESKey();
		String wrapped = wrap(password, salt, skey);
		return new String[] { password, salt, wrapped };
	}

	/**
	 * for temp storage
	 * @return
	 * @throws Exception 
	 */
	public static String[] serializeKey(Key key) throws Exception {
		String pwd = tempPassword();
		String salt = salt();
		String wrapped = wrap(pwd, salt, key);
		return new String[] { pwd, salt, wrapped };
	}
	
	public static Key deserializeKey(String[] sa) throws Exception {
		return unwrap(sa);
	}
	
	public static void list(File top, File ezip, Key key, String ext,
			StatusMonitor status, List<FileItem> list) throws Exception {
		ext = (ext == null ? "" : ext);
		try {
			Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key);
			//
			FileInputStream fis = new FileInputStream(ezip);
			BufferedInputStream bis = new BufferedInputStream(fis, BUFSIZE);
			CipherInputStream cin = new CipherInputStream(bis, cipher);
			ZipInputStream zis = new ZipInputStream(cin);
			ZipEntry ze = zis.getNextEntry();
			while (ze != null) {
				if (!ze.isDirectory()) {
					if (status != null) {
						status.send(ze.getName());
					}
					FileItem fi = new FileItem(ze.getName());
					list.add(fi);
				}
				ze = zis.getNextEntry();
			}
			zis.close();
		} catch (Exception e) {
			// e.printStackTrace();
			throw e;
		}
	}

	public static byte[] random(int len) throws Exception {
		SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
		byte[] b = new byte[len];
		rand.nextBytes(b);
		return b;
	}

	public static String tempPassword() throws Exception {
		byte[] b = random(LEN);
		String enc = Base64.encodeBytes(b);
		return enc;
	}

	public static String salt() throws Exception {
		byte[] b = random(LEN);
		String enc = Base64.encodeBytes(b);
		return enc;
	}

	private static Key toPBEKey(String password, byte[] salt) throws Exception {
		PBEKeySpec pks = new PBEKeySpec(password.toCharArray(), salt,
				ITERATION_COUNT, 128);
		SecretKeyFactory skf = SecretKeyFactory.getInstance(PBE_ALG);
		Key skey = skf.generateSecret(pks);

		byte[] enckey = skey.getEncoded();
		System.out.println("enckey: " + new String(Base64.encodeBytes(enckey)));

		return skey;
	}

	/**
	 * 
	 * @param top
	 *            parent folder in which to extract files
	 * @param ezip
	 *            encrypted ZIP file
	 * @param skey
	 *            secret key
	 * @param filekey
	 *            temporary key for encrypting the extracted files in zip
	 * @param ext
	 *            file extension appended to original file name
	 * 
	 * @return
	 * @throws Exception
	 */
	public static void unzip(File top, File ezip, Key skey,
			Key filekey, String ext, StatusMonitor status, List<FileItem> list) throws Exception {
		try {
			Cipher cipher = createCipher(Cipher.DECRYPT_MODE, skey);
			//
			FileInputStream fis = new FileInputStream(ezip);
			CipherInputStream cin = new CipherInputStream(fis, cipher);
			ZipInputStream zis = new ZipInputStream(cin);
			ZipEntry ze = zis.getNextEntry();
			while (ze != null) {

				if (!ze.isDirectory()) {
					String name = ze.getName();
					if (Debug.DEBUG) {
						Log.d("TAG", " unzip: " + name);
					}
					if (!name.equals("MANIFEST.MF")) {
						if (status != null) {
							status.send(name);
						}
						File f = new File(top, name + ext);
						File dir = f.getParentFile();
						if (dir != null && !dir.exists()) {
							dir.mkdirs();
						}
						FileOutputStream fos = new FileOutputStream(f);
						Cipher ecipher = createCipher(Cipher.ENCRYPT_MODE, filekey);
						CipherOutputStream cos = new CipherOutputStream(fos,
								ecipher);
						int read = 0;
						final byte[] buf = new byte[BUFSIZE];
						while ((read = zis.read(buf, 0, BUFSIZE)) > -1) {
							cos.write(buf, 0, read);
						}
						cos.close();
						long modified = ze.getTime();
						f.setLastModified(modified);
						//
						FileItem fi = new FileItem(name);
						fi.setModified(modified);
						
						list.add(fi);
					}
				}
				zis.closeEntry();
				ze = zis.getNextEntry();
			}
			zis.close();
		} catch (Exception e) {
			// e.printStackTrace();
			throw e;
		}
	}

	public static ZipInputStream unzip(File ezip, Key skey, String name)
			throws Exception {
		try {
			Cipher cipher = createCipher(Cipher.DECRYPT_MODE, skey);
			//
			FileInputStream fis = new FileInputStream(ezip);
			CipherInputStream cin = new CipherInputStream(fis, cipher);
			ZipInputStream zis = new ZipInputStream(cin);
			ZipEntry ze = zis.getNextEntry();
			while ((ze = zis.getNextEntry()) != null) {
				if (!ze.isDirectory()) {
					String n = ze.getName();
					
					boolean same = ZipUtils.sameEntry(n, name); // n.equals(name)
					if (same) {
						if (Debug.DEBUG) {
							Log.d(TAG, "entry; " + n + " name: " + name);
						}
						// found
						return zis;
					}
				}
				zis.closeEntry();
			}
			zis.close();
		} catch (Exception e) {
			// e.printStackTrace();
			throw e;
		}
		return null;
	}

	public static String wrap(String password, String salt, Key secret)
			throws Exception {
		byte[] dsalt = Base64.decode(salt);
		Key pk = toPBEKey(password, dsalt);
		//
		PBEParameterSpec spec = new PBEParameterSpec(dsalt, ITERATION_COUNT);
		Cipher c = Cipher.getInstance(PBE_ALG);
		c.init(Cipher.WRAP_MODE, pk, spec);
		byte[] wk = c.wrap(secret);
		return Base64.encodeBytes(wk);
	}

	public static Key unwrap(String[] key) throws Exception {
		return unwrap(key[0], key[1], key[2]);
	}

	public static Key unwrap(String password, String salt, String secret)
			throws Exception {
		byte[] dsalt = Base64.decode(salt);
		Key pk = toPBEKey(password, dsalt);
		//
		PBEParameterSpec spec = new PBEParameterSpec(dsalt, ITERATION_COUNT);
		Cipher c = Cipher.getInstance(PBE_ALG);
		c.init(Cipher.UNWRAP_MODE, pk, spec);
		byte[] k = Base64.decode(secret);
		Key uwk = c.unwrap(k, "AES", Cipher.SECRET_KEY);
		return uwk;
	}

	public static InputStream openInputStream(File file, Key skey)
			throws Exception {
		Cipher cipher = CryptoUtils.createCipher(Cipher.DECRYPT_MODE, skey);
		FileInputStream fis = new FileInputStream(file);
		CipherInputStream cin = new CipherInputStream(fis, cipher);
		return cin;
	}

	public static void extractFile(File ezip, File dest, Key skey, Key filekey,
			String name) throws Exception {
		try {
			Cipher cipher = createCipher(Cipher.DECRYPT_MODE, skey);
			//
			FileInputStream fis = new FileInputStream(ezip);
			CipherInputStream cin = new CipherInputStream(fis, cipher);
			ZipInputStream zis = new ZipInputStream(cin);
			ZipEntry ze = zis.getNextEntry();
			while ((ze = zis.getNextEntry()) != null) {
				if (!ze.isDirectory()) {
					String n = ze.getName();
					boolean same = ZipUtils.sameEntry(n, name); // n.equals(name)
//					if (Debug.DEBUG) {
//						Log.d(TAG, "entry " + n + " name: " + name);
//					}
					if (same) {
						if (Debug.DEBUG) {
							Log.d(TAG, "entry found: " + n + " name: " + name);
						}
						// found
						File dir = dest.getParentFile();
						if (dir != null && !dir.exists()) {
							dir.mkdirs();
						}
						FileOutputStream fos = new FileOutputStream(dest);
						Cipher ecipher = createCipher(Cipher.ENCRYPT_MODE,
								filekey);
						CipherOutputStream cos = new CipherOutputStream(fos,
								ecipher);
						byte[] data = new byte[BUFSIZE];
						try {
							for (int read; (read = zis.read(data, 0, BUFSIZE)) != -1;) {
								cos.write(data, 0, read);
							}
						} finally {
							cos.close();
						}
						dest.setLastModified(ze.getTime());
						zis.closeEntry();
						return;
					}
				}
				zis.closeEntry();
			}
			zis.close();
		} catch (Exception e) {
			// e.printStackTrace();
			throw e;
		}
	}

	private CryptoUtils() {
	}
}
